/*
 * @(#)AbstractQueryRunner.java
 * Date 2013-08-31
 * Version 1.0
 * Author Jim Horner
 * Copyright (c)2012
 */


package us.hornerscorners.lamppost.registry.query.es;

import java.text.ParseException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import org.apache.commons.lang3.time.DateUtils;

import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.IdsQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import us.hornerscorners.lamppost.exception.ConversionException;
import us.hornerscorners.lamppost.exception.ServiceActionError;
import us.hornerscorners.lamppost.exception.ServiceActionException;
import us.hornerscorners.lamppost.exception.SeverityType;

import us.hornerscorners.lamppost.hl7v3common.utils.HL7Utils;

import us.hornerscorners.lamppost.ihecommon.config.ElasticSearchConfig;
import us.hornerscorners.lamppost.ihecommon.config.IHEConfig;
import us.hornerscorners.lamppost.ihecommon.config.IHEServiceType;
import us.hornerscorners.lamppost.ihecommon.config.RegistryServiceConfig;

import us.hornerscorners.lamppost.ihecommon.service.ElasticSearchSource;

import us.hornerscorners.lamppost.model.common.CodedElement;
import us.hornerscorners.lamppost.model.common.IPatientAware;
import us.hornerscorners.lamppost.model.common.InstanceIdentifier;
import us.hornerscorners.lamppost.model.common.PersistableModelType;

import us.hornerscorners.lamppost.registry.config.QueryMethodConfig;
import us.hornerscorners.lamppost.registry.config.QueryParameterConfig;

import us.hornerscorners.lamppost.registry.constant.ErrorCode;
import us.hornerscorners.lamppost.registry.constant.QueryField;
import us.hornerscorners.lamppost.registry.constant.QuerySlotName;

import us.hornerscorners.lamppost.registry.model.QueryParameter;
import us.hornerscorners.lamppost.registry.model.QueryRequest;
import us.hornerscorners.lamppost.registry.model.QueryResponse;
import us.hornerscorners.lamppost.registry.model.QueryReturnType;
import us.hornerscorners.lamppost.registry.model.RegistryObjects;

import us.hornerscorners.lamppost.registry.transformer.model.CodedElementTransformer;
import us.hornerscorners.lamppost.registry.transformer.model.PatientIdentifierTransformer;

import us.hornerscorners.lamppost.registry.utils.XDSUtils;

import us.hornerscorners.lamppost.utils.UTCDateUtils;

import static us.hornerscorners.lamppost.model.common.PersistableModelType.Folder;


/**
 * Class description
 *
 *
 * @version        v1.0, 2013-08-31
 * @author         Jim Horner
 */
public abstract class AbstractQueryRunner implements IQueryRunner {

    /** Field description */
    private final CodedElementTransformer codedElementTransformer;

    /** Field description */
    private final IHEConfig config;

    /** Field description */
    private final Logger logger = LoggerFactory.getLogger(getClass());

    /** Field description */
    private final PatientIdentifierTransformer patientIdentifierTransformer;

    /** Field description */
    private final RegistryServiceConfig registryServiceConfig;

    /** Field description */
    private final ElasticSearchSource source;

    /**
     * Constructs ...
     *
     *
     * @param config
     * @param elasticSearchSource
     * @param codedElementTransformer
     * @param patientIdentifierTransformer
     */
    public AbstractQueryRunner(
            IHEConfig config, ElasticSearchSource elasticSearchSource,
            CodedElementTransformer codedElementTransformer,
            PatientIdentifierTransformer patientIdentifierTransformer) {

        super();
        this.config = config;
        this.source = elasticSearchSource;
        this.codedElementTransformer = codedElementTransformer;
        this.patientIdentifierTransformer = patientIdentifierTransformer;

        this.registryServiceConfig =
            (RegistryServiceConfig) this.config.findServiceConfig(
                IHEServiceType.Registry);
    }

    /**
     * Method description
     *
     *
     * @param request
     * @param slotName
     * @param attrCode
     * @param attrCodeSystem
     *
     * @return
     */
    protected QueryBuilder createCodedElementQuery(QueryRequest request,
            QuerySlotName slotName, QueryField attrCode, QueryField attrCodeSystem) {

        QueryBuilder result = null;

        QueryParameterConfig paramConfig = request.getQueryParameterConfig(slotName);

        if (paramConfig.isMultipleAllowed() && request.hasMultipleValues(slotName)) {

            if (paramConfig.isAndOrSupported()
                    && request.hasMultipleSlotValues(slotName)) {

                result = QueryBuilders.boolQuery();

                for (QueryParameter parameter : request.getParameters(slotName)) {

                    List<String> values = parameter.getValues();

                    ((BoolQueryBuilder) result).must(createCodedElementQuery(slotName,
                            values, attrCode, attrCodeSystem));
                }

            } else {

                List<String> values = request.getFirstSlotValues(slotName);

                result = createCodedElementQuery(slotName, values, attrCode,
                                                 attrCodeSystem);
            }

        } else {

            String value = request.getFirstParameterValue(slotName);

            result = createCodedElementQuery(slotName, value, attrCode, attrCodeSystem);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param values
     * @param attrCode
     * @param attrCodeSystem
     *
     * @return
     */
    protected QueryBuilder createCodedElementQuery(QuerySlotName slotName,
            Collection<String> values, QueryField attrCode, QueryField attrCodeSystem) {

        BoolQueryBuilder result = QueryBuilders.boolQuery();

        result.minimumNumberShouldMatch(1);

        for (String value : values) {

            BoolQueryBuilder ceQuery = createCodedElementQuery(slotName, value, attrCode,
                                           attrCodeSystem);

            result.should(ceQuery);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param value
     * @param attrCode
     * @param attrCodeSystem
     *
     * @return
     */
    protected BoolQueryBuilder createCodedElementQuery(QuerySlotName slotName,
            String value, QueryField attrCode, QueryField attrCodeSystem) {

        BoolQueryBuilder result = QueryBuilders.boolQuery();
        CodedElement ce = transformCEString(slotName, value);

        result.must(QueryBuilders.termQuery(attrCode.getField(), ce.getCode()));

        if (StringUtils.isNotBlank(ce.getCodeSystem())) {

            result.must(QueryBuilders.termQuery(attrCodeSystem.getField(),
                    ce.getCodeSystem()));
        }

        return result;
    }

    /**
     * Method description
     *
     *
     *
     * @param modelType
     * @param ids
     * @return
     */
    protected QueryBuilder createEntryURNQuery(PersistableModelType modelType,
            Collection<String> ids) {

        ElasticSearchConfig escfg =
            (ElasticSearchConfig) this.config.getDataStoreConfig();
        String type = escfg.getModelType(modelType);

        IdsQueryBuilder result = QueryBuilders.idsQuery(type);

        result.addIds(ids.toArray(new String[ids.size()]));

        return result;
    }

    /**
     * Method description
     *
     *
     * @param request
     * @param slotName
     *
     * @return
     */
    protected QueryBuilder createEntryURNQuery(QueryRequest request,
            PersistableModelType modelType, QuerySlotName slotName) {

        QueryBuilder result = null;

        QueryParameterConfig config = request.getQueryParameterConfig(slotName);

        if (config.isMultipleAllowed() && request.hasMultipleValues(slotName)) {

            if (config.isAndOrSupported() && request.hasMultipleSlotValues(slotName)) {

                result = QueryBuilders.boolQuery();

                for (QueryParameter parameter : request.getParameters(slotName)) {

                    List<String> values = parameter.getValues();

                    ((BoolQueryBuilder) result).must(createEntryURNQuery(modelType,
                            values));
                }

            } else {

                List<String> values = request.getFirstSlotValues(slotName);

                result = createEntryURNQuery(modelType, values);
            }

        } else {

            String value = request.getFirstParameterValue(slotName);

            result = createEntryURNQuery(modelType, Collections.singletonList(value));
        }

        return result;
    }

    /**
     * Method description
     *
     *
     *
     * @return
     */
    protected QueryBuilder createIdentifierQuery(InstanceIdentifier id,
            QueryField attrRoot, QueryField attrExtension) {

        BoolQueryBuilder result = QueryBuilders.boolQuery();

        result.must(QueryBuilders.termQuery(attrRoot.getField(), id.getRoot()));

        if (StringUtils.isNotBlank(id.getExtension())) {

            result.must(QueryBuilders.termQuery(attrExtension.getField(),
                    id.getExtension()));
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param request
     *
     * @return
     */
    protected QueryBuilder createLocalTimeQuery(QueryRequest request,
            QuerySlotName fromSlotName, QuerySlotName toSlotName, QueryField attrName) {

        RangeQueryBuilder result = QueryBuilders.rangeQuery(attrName.getField());

        result.includeLower(true);
        result.includeUpper(false);

        if (request.hasValues(fromSlotName)) {

            Date value = null;

            try {

                value = DateUtils.parseDate(request.getFirstParameterValue(fromSlotName),
                                            UTCDateUtils.DATE_TIME_FORMATS);

                result.from(new DateTime(value).withZone(DateTimeZone.UTC));

            } catch (ParseException e) {

                // do nothing
            }

        }

        if (request.hasValues(toSlotName)) {

            Date value = null;

            try {

                value = DateUtils.parseDate(request.getFirstParameterValue(toSlotName),
                                            UTCDateUtils.DATE_TIME_FORMATS);

                result.to(new DateTime(value).withZone(DateTimeZone.UTC));

            } catch (ParseException e) {

                // do nothing
            }
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param request
     * @param slotName
     * @param attrRoot
     * @param attrExtension
     *
     * @return
     */
    protected QueryBuilder createPatientIdentifierQuery(QueryRequest request,
            QuerySlotName slotName, QueryField attrRoot, QueryField attrExtension) {

        QueryBuilder result = null;

        QueryParameterConfig config = request.getQueryParameterConfig(slotName);

        if (config.isMultipleAllowed() && request.hasMultipleValues(slotName)) {

            if (config.isAndOrSupported() && request.hasMultipleSlotValues(slotName)) {

                result = QueryBuilders.boolQuery();

                for (QueryParameter parameter : request.getParameters(slotName)) {

                    List<String> values = parameter.getValues();

                    ((BoolQueryBuilder) result).must(
                        createPatientIdentifierQuery(
                            slotName, values, attrRoot, attrExtension));
                }

            } else {

                List<String> values = request.getFirstSlotValues(slotName);

                result = createPatientIdentifierQuery(slotName, values, attrRoot,
                        attrExtension);
            }

        } else {

            String value = request.getFirstParameterValue(slotName);

            InstanceIdentifier id = transformPIDCXString(slotName, value);

            result = createIdentifierQuery(id, attrRoot, attrExtension);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param slotName
     * @param values
     * @param attrRoot
     * @param attrExtension
     *
     * @return
     */
    protected QueryBuilder createPatientIdentifierQuery(QuerySlotName slotName,
            Collection<String> values, QueryField attrRoot, QueryField attrExtension) {

        BoolQueryBuilder result = QueryBuilders.boolQuery();

        result.minimumNumberShouldMatch(1);

        for (String value : values) {

            InstanceIdentifier id = transformPIDCXString(slotName, value);

            QueryBuilder idQuery = createIdentifierQuery(id, attrRoot, attrExtension);

            result.should(idQuery);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param checkWildcard
     * @param values
     * @param attr
     *
     * @return
     */
    protected QueryBuilder createStringQuery(boolean checkWildcard,
            Collection<String> values, QueryField attr) {

        QueryBuilder result = null;

        if (checkWildcard) {

            result = QueryBuilders.boolQuery();

            ((BoolQueryBuilder) result).minimumNumberShouldMatch(1);

            for (String value : values) {

                QueryBuilder stQuery = createStringQuery(checkWildcard, value, attr);

                ((BoolQueryBuilder) result).should(stQuery);
            }

        } else {

            result = QueryBuilders.termsQuery(attr.getField(), values);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param checkWildcard
     * @param value
     * @param attr
     *
     * @return
     */
    protected QueryBuilder createStringQuery(boolean checkWildcard, String value,
            QueryField attr) {

        QueryBuilder result = null;

        if (checkWildcard && StringUtils.containsAny(value, '_', '%')) {

            value = StringUtils.replace(value, "_", "?");
            value = StringUtils.replace(value, "%", "*");

            result = QueryBuilders.wildcardQuery(attr.getField(), value);

        } else {

            result = QueryBuilders.termQuery(attr.getField(), value);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param request
     * @param slotName
     * @param attr
     *
     * @return
     */
    protected QueryBuilder createStringQuery(QueryRequest request,
            QuerySlotName slotName, QueryField attr) {

        QueryBuilder result = null;

        QueryParameterConfig paramConfig = request.getQueryParameterConfig(slotName);
        boolean checkWildcard = paramConfig.isSqlLikeSupported();

        if (paramConfig.isMultipleAllowed()) {

            if (paramConfig.isAndOrSupported()) {

                result = QueryBuilders.boolQuery();

                for (QueryParameter parameter : request.getParameters(slotName)) {

                    List<String> values = parameter.getValues();

                    ((BoolQueryBuilder) result).must(createStringQuery(checkWildcard,
                            values, attr));
                }

            } else {

                List<String> values = request.getFirstSlotValues(slotName);

                result = createStringQuery(checkWildcard, values, attr);
            }

        } else {

            String value = request.getFirstParameterValue(slotName);

            result = createStringQuery(checkWildcard, value, attr);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param request
     * @param fromSlotName
     * @param toSlotName
     * @param attrName
     *
     * @return
     */
    protected QueryBuilder createTimeQuery(QueryRequest request,
            QuerySlotName fromSlotName, QuerySlotName toSlotName, QueryField attrName) {

        RangeQueryBuilder result = QueryBuilders.rangeQuery(attrName.getField());

        result.includeLower(true);
        result.includeUpper(false);

        if (request.hasValues(fromSlotName)) {

            DateTime value =
                XDSUtils.parseDTM(request.getFirstParameterValue(fromSlotName));

            result.from(value);
        }

        if (request.hasValues(toSlotName)) {

            DateTime value =
                XDSUtils.parseDTM(request.getFirstParameterValue(toSlotName));

            result.to(value);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param values
     * @param attrRoot
     * @param attrExtension
     *
     * @return
     */
    protected QueryBuilder createUniqueIdentifierQuery(Collection<String> values,
            QueryField attrRoot, QueryField attrExtension) {

        BoolQueryBuilder result = QueryBuilders.boolQuery();

        result.minimumNumberShouldMatch(1);

        for (String value : values) {

            InstanceIdentifier id = new InstanceIdentifier();

            id.setRoot(value);

            QueryBuilder idQuery = createIdentifierQuery(id, attrRoot, attrExtension);

            result.should(idQuery);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param request
     * @param slotName
     * @param attrRoot
     * @param attrExtension
     *
     * @return
     */
    protected QueryBuilder createUniqueIdentifierQuery(QueryRequest request,
            QuerySlotName slotName, QueryField attrRoot, QueryField attrExtension) {

        QueryBuilder result = null;

        QueryParameterConfig config = request.getQueryParameterConfig(slotName);

        if (config.isMultipleAllowed()) {

            if (config.isAndOrSupported()) {

                result = QueryBuilders.boolQuery();

                for (QueryParameter parameter : request.getParameters(slotName)) {

                    List<String> values = parameter.getValues();

                    ((BoolQueryBuilder) result).must(createUniqueIdentifierQuery(values,
                            attrRoot, attrExtension));
                }

            } else {

                List<String> values = request.getFirstSlotValues(slotName);

                result = createUniqueIdentifierQuery(values, attrRoot, attrExtension);
            }

        } else {

            String value = request.getFirstParameterValue(slotName);

            InstanceIdentifier id = new InstanceIdentifier();

            id.setRoot(value);
            result = createIdentifierQuery(id, attrRoot, attrExtension);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @return
     */
    protected Class getErrorLocation() {

        return getClass();
    }

    /**
     * Method description
     *
     *
     * @return
     */
    protected RegistryServiceConfig getRegistryServiceConfig() {

        return this.registryServiceConfig;
    }

    /**
     * Method description
     *
     *
     * @return
     */
    protected ElasticSearchSource getSource() {
        return source;
    }

    /**
     * Method description
     *
     *
     * @param request
     *
     * @return
     *
     * @throws ServiceActionException
     */
    @Override
    public QueryResponse run(QueryRequest request) throws ServiceActionException {

        Map<PersistableModelType, QueryBuilder> queries = createQuery(request);

        return new QueryResponse(request, runQueries(queries));
    }

    /**
     * Method description
     *
     *
     * @param queries
     *
     * @return
     *
     * @throws ServiceActionException
     */
    @SuppressWarnings("unchecked")
    protected RegistryObjects runQueries(Map<PersistableModelType, QueryBuilder> queries)
            throws ServiceActionException {

        RegistryObjects result = new RegistryObjects();

        for (PersistableModelType type : queries.keySet()) {

            QueryBuilder query = queries.get(type);

            switch (type) {

                case DocumentEntry :
                    result.addAllDocumentEntries(getSource().runQuery(query,
                            type.getPersistableClass()));

                    break;

                case Folder :
                    result.addAllFolders(getSource().runQuery(query,
                            type.getPersistableClass()));

                    break;

                case SubmissionSet :
                    result.addAllSubmissionSets(getSource().runQuery(query,
                            type.getPersistableClass()));

                    break;
            }
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param ceString
     *
     * @return
     */
    private CodedElement transformCEString(QuerySlotName slotName, String ceString) {

        CodedElement result = null;

        try {

            result = this.codedElementTransformer.transform(slotName, ceString);

        } catch (ConversionException e) {

            // should not happen
            logger.warn("Exception creating codedElement.", e);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     *
     * @return
     */
    protected InstanceIdentifier transformPIDCXString(QuerySlotName slotName,
            String pidString) {

        InstanceIdentifier result = null;

        try {

            result = this.patientIdentifierTransformer.transform(slotName, pidString);

        } catch (ServiceActionException e) {

            // should not happen
            logger.warn("Exception creating identifier.", e);
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param response
     *
     * @throws ServiceActionException
     */
    @Override
    public Collection<ServiceActionError> validateResponse(QueryResponse response)
            throws ServiceActionException {

        List<ServiceActionError> result = new ArrayList<>();

        QueryRequest request = response.getQueryRequest();
        QueryMethodConfig cfg = request.getQueryMethodConfig();

        RegistryObjects rol = response.getRegistryObjects();

        if (QueryReturnType.LeafClass.equals(request.getReturnType())
                && cfg.isCheckPatientId()) {

            // check patient identifier consistency
            InstanceIdentifier pid = null;

            for (IPatientAware paware : rol.getAllPatientAwareables()) {

                if (pid == null) {

                    pid = paware.getPatientId();

                } else {

                    if (pid.isNotSameId(paware.getPatientId())) {

                        String msg =
                            String.format(
                                "Result set does not contain a single patient. Identifier [%s] and [%s] were found.",
                                HL7Utils.toCXString(pid),
                                HL7Utils.toCXString(paware.getPatientId()));

                        ServiceActionError error =
                            new ServiceActionError(
                                ErrorCode.XDSResultNotSinglePatient.getCode(), msg,
                                SeverityType.Error);

                        result.add(error);
                    }
                }
            }
        }

        return result;
    }
}
